<template>
  <div class="w-tooltip-wrap" ref="wrapperRef" :class="{ 'w-tooltip-wrap--attached': !detachTo }">
    <slot name="activator" :on="eventHandlers"></slot>
    <transition :name="transitionName">
      <div class="w-tooltip" :ref="el => (tooltipEl = el)" :key="_.uid" v-show="showTooltip" :class="classes"
        :style="styles">
        <div v-if="bgColor" :class="color">
          <slot></slot>
        </div>
        <slot v-else></slot>
      </div>
    </transition>
  </div>
</template>

<script>
  /**
   * Complexity of this component: Vue 2.x can only mount 1 single root element, but we don't
   * want to wrap the activator as it may break the layout.
   * Another simpler way would be to append the tooltip inside the activator, but some HTML tags
   * can't have children like <input>.
   * So a solution is to mount both the activator element and the tooltip in a wrapper then unwrap
   * and move the tooltip elsewhere in the DOM.
   */

  import { consoleWarn } from "../../utils/console";
  import {
    ref,
    reactive,
    computed,
    watch,
    onMounted,
    onBeforeUnmount,
    getCurrentInstance
  } from "vue";
  const marginFromWindowSide = 4; // Amount of px from a window side, instead of overflowing.

  export default {
    name: "w-tooltip",

    props: {
      modelValue: {},
      showOnClick: { type: Boolean },
      color: { type: String },
      bgColor: { type: String },
      noBorder: { type: Boolean },
      shadow: { type: Boolean },
      tile: { type: Boolean },
      round: { type: Boolean },
      transition: { type: String, default: "" },
      tooltipClass: { type: String },
      // Position.
      detachTo: {},
      fixed: { type: Boolean },
      top: { type: Boolean },
      bottom: { type: Boolean },
      left: { type: Boolean },
      right: { type: Boolean },
      zIndex: { type: [Number, String, Boolean] }
    },

    emits: [],
    setup(props, ctx) {
      const instance = getCurrentInstance();
      const showTooltip = ref(false);
      // The activator coordinates.
      const coordinates = reactive({
        top: 0,
        left: 0,
        width: 0,
        height: 0
      });
      const activatorEl = ref(null);
      const tooltipEl = ref(null);
      const timeoutId = ref(null);
      const wrapperRef = ref(null);

      const transitionName = computed(() => {
        const direction = position.value.replace(
          /top|bottom/,
          m => ({ top: "up", bottom: "down" }[m])
        );
        return props.transition || `w-tooltip-slide-fade-${direction}`;
      });

      const detachToTarget = computed(() => {
        let target = props.detachTo || ".w-app";
        if (target === true) target = ".w-app";
        else if (target && !["object", "string"].includes(typeof target))
          target = ".w-app";
        else if (typeof target === "object" && !target.nodeType) {
          target = ".w-app";
          consoleWarn(
            "Invalid node provided in w-tooltip `attach-to`. Falling back to .w-app.",
            instance.ctx
          );
        }
        if (typeof target === "string") target = document.querySelector(target);

        if (!target) {
          consoleWarn(
            `Unable to locate ${props.detachTo ? `target ${props.detachTo}` : ".w-app"
            }`,
            instance.ctx
          );
          target = document.querySelector(".w-app");
        }

        return target;
      });

      // DOM element that will receive the tooltip.
      const tooltipParentEl = computed(() => {
        return props.detachTo ? detachToTarget.value : wrapperRef.value;
      });

      const position = computed(() => {
        return (
          (props.top && "top") ||
          (props.bottom && "bottom") ||
          (props.left && "left") ||
          (props.right && "right") ||
          "bottom"
        );
      });

      const tooltipCoordinates = computed(() => {
        const coords = {};
        const { top, left, width, height } = coordinates;

        switch (position.value) {
          case "top": {
            coords.top = top;
            coords.left = left + width / 2; // left: 50%.
            break;
          }
          case "bottom": {
            coords.top = top + height;
            coords.left = left + width / 2; // left: 50%.
            break;
          }
          case "left": {
            coords.top = top + height / 2; // top: 50%.
            coords.left = left;
            break;
          }
          case "right": {
            coords.top = top + height / 2; // top: 50%.
            coords.left = left + width;
            break;
          }
        }

        return coords;
      });

      const classes = computed(() => {
        return {
          [props.color]: !props.bgColor,
          [`${props.bgColor} ${props.bgColor}--bg`]: props.bgColor,
          [props.tooltipClass]: props.tooltipClass,
          [`w-tooltip--${position.value}`]: true,
          "w-tooltip--tile": props.tile,
          "w-tooltip--round": props.round,
          "w-tooltip--shadow": props.shadow,
          "w-tooltip--fixed": props.fixed,
          "w-tooltip--active": showTooltip.value,
          "w-tooltip--no-border": props.noBorder || props.bgColor,
          "w-tooltip--custom-transition": props.transition
        };
      });

      const styles = computed(() => {
        return {
          zIndex: props.zIndex || props.zIndex === 0 || null,
          top: `${~~tooltipCoordinates.value.top}px`,
          left: `${~~tooltipCoordinates.value.left}px`
        };
      });

      const eventHandlers = computed(() => {
        let handlers = {};
        if (props.showOnClick) handlers = { click: toggle };
        else {
          handlers = {
            focus: toggle,
            blur: toggle,
            mouseenter: toggle,
            mouseleave: toggle
          };
          if ("ontouchstart" in window) {
            handlers.click = toggle;
          }
        }
        return handlers;
      });

      function toggle(e) {
        let shouldShowTooltip = showTooltip.value;
        if ("ontouchstart" in window) {
          if (e.type === "click") {
            shouldShowTooltip = !shouldShowTooltip;
          }
        } else if (e.type === "click" && props.showOnClick)
          shouldShowTooltip = !shouldShowTooltip;
        else if (["mouseenter", "focus"].includes(e.type) && !props.showOnClick)
          shouldShowTooltip = true;
        else if (["mouseleave", "blur"].includes(e.type) && !props.showOnClick)
          shouldShowTooltip = false;

        timeoutId.value = clearTimeout(timeoutId.value);
        if (shouldShowTooltip) {
          const { top, left, width, height } = getCoordinates(e);
          coordinates.top = top;
          coordinates.left = left;
          coordinates.width = width;
          coordinates.height = height;
          // In `getCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
          // if we don't postpone the tooltip apparition it will start transition from a visible tooltip and
          // thus will not transition.
          timeoutId.value = setTimeout(() => (showTooltip.value = true), 10);
        } else showTooltip.value = false;
      }

      function getCoordinates() {
        const {
          top,
          left,
          width,
          height
        } = activatorEl.value.getBoundingClientRect();
        let coords = { top, left, width, height };

        if (!props.fixed) {
          const {
            top: targetTop,
            left: targetLeft
          } = tooltipParentEl.value.getBoundingClientRect();
          coords = { ...coords, top: top - targetTop, left: left - targetLeft };
        }

        const tooltip = tooltipEl.value;

        // 1. First display the tooltip but hide it (So we can get its dimension).
        tooltip.style.visibility = "hidden";
        tooltip.style.display = "table";
        const computedStyles = window.getComputedStyle(tooltip, null);

        // Keep fully in viewport.
        // --------------------------------------------------
        if (position.value === "top" && top - tooltip.offsetHeight < 0) {
          const margin = -parseInt(computedStyles.getPropertyValue("margin-top"));
          coords.top -=
            top - tooltip.offsetHeight - margin - marginFromWindowSide;
        } else if (position.value === "left" && left - tooltip.offsetWidth < 0) {
          const margin = -parseInt(
            computedStyles.getPropertyValue("margin-left")
          );
          coords.left -=
            left - tooltip.offsetWidth - margin - marginFromWindowSide;
        } else if (
          position.value === "right" &&
          left + width + tooltip.offsetWidth > window.innerWidth
        ) {
          const margin = parseInt(computedStyles.getPropertyValue("margin-left"));
          coords.left -=
            left +
            width +
            tooltip.offsetWidth -
            window.innerWidth +
            margin +
            marginFromWindowSide;
        } else if (
          position.value === "bottom" &&
          top + height + tooltip.offsetHeight > window.innerHeight
        ) {
          const margin = parseInt(computedStyles.getPropertyValue("margin-top"));
          coords.top -=
            top +
            height +
            tooltip.offsetHeight -
            window.innerHeight +
            margin +
            marginFromWindowSide;
        }
        // --------------------------------------------------

        // 2. Update left & top if there is a custom transition.
        // Tooltip position relies on transform translate, the custom animation may override the transform
        // property so do without it and subtract half width or height manually.
        if (props.transition) {
          // If tooltip is on top or bottom.
          if (["top", "bottom"].includes(position.value))
            coords.left -= tooltip.offsetWidth / 2;
          // If tooltip is on left or right.
          if (["left", "right"].includes(position.value))
            coords.top -= tooltip.offsetHeight / 2;

          if (position.value === "left") coords.left -= tooltip.offsetWidth;
          if (position.value === "top") coords.top -= tooltip.offsetHeight;
        }

        // 3. Hide the tooltip again so the transition happens correctly.
        tooltip.style.visibility = null;
        tooltip.style.display = "none";

        return coords;
      }

      function insertTooltip() {
        const wrapper = wrapperRef.value;

        // Unwrap the activator element.
        wrapper.parentNode.insertBefore(activatorEl.value, wrapper);

        // Move the tooltip elsewhere in the DOM.
        // wrapper.parentNode.insertBefore(this.tooltipEl, wrapper)
        // this.tooltipEl is set in the dynamic ref.
        detachToTarget.value.appendChild(tooltipEl.value);
      }

      function removeTooltip() {
        // el.remove() doesn't work on IE11.
        if (tooltipEl.value && tooltipEl.value.parentNode)
          tooltipEl.value.parentNode.removeChild(tooltipEl.value);
      }

      onMounted(() => {
        activatorEl.value = wrapperRef.value.firstElementChild;
        if (props.detachTo) {
          insertTooltip();
        }

        if (props.modelValue) {
          toggle({ type: "click", target: activatorEl.value });
        }
      });
      onBeforeUnmount(() => {
        removeTooltip();

        // el.remove() doesn't work on IE11.
        if (activatorEl.value && activatorEl.value.parentNode) {
          activatorEl.value.parentNode.removeChild(activatorEl.value);
        }
      });

      watch(
        () => props.modelValue,
        () => {
          toggle({ type: "click", target: activatorEl.value });
        }
      );

      watch(
        () => props.detachTo,
        () => {
          removeTooltip();
          insertTooltip();
        }
      );

      return {
        showTooltip,
        coordinates,
        activatorEl,
        tooltipEl,
        timeoutId,
        // refs
        wrapperRef,
        // computed
        transitionName,
        detachToTarget,
        tooltipParentEl,
        position,
        tooltipCoordinates,
        classes,
        styles,
        eventHandlers,
        // methods
        toggle,
        getCoordinates,
        insertTooltip,
        removeTooltip
      };
    }

  };
</script>